{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "4f3fce77",
   "metadata": {
    "id": "AuW-xg_bTsaF"
   },
   "source": [
    "# Week 1: Using CNN's with the Cats vs Dogs Dataset\n",
    "\n",
    "Welcome to the 1st assignment of the course! This week, you will be using the famous `Cats vs Dogs` dataset to train a model that can classify images of dogs from images of cats. For this, you will create your own Convolutional Neural Network in Tensorflow and leverage Keras' image preprocessing utilities."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1cf70ea2",
   "metadata": {},
   "source": [
    "#### TIPS FOR SUCCESSFUL GRADING OF YOUR ASSIGNMENT:\n",
    "\n",
    "- All cells are frozen except for the ones where you need to submit your solutions or when explicitly mentioned you can interact with it.\n",
    "\n",
    "- You can add new cells to experiment but these will be omitted by the grader, so don't rely on newly created cells to host your solution code, use the provided places for this.\n",
    "\n",
    "- You can add the comment # grade-up-to-here in any graded cell to signal the grader that it must only evaluate up to that point. This is helpful if you want to check if you are on the right track even if you are not done with the whole assignment. Be sure to remember to delete the comment afterwards!\n",
    "\n",
    "- Avoid using global variables unless you absolutely have to. The grader tests your code in an isolated environment without running all cells from the top. As a result, global variables may be unavailable when scoring your submission. Global variables that are meant to be used will be defined in UPPERCASE.\n",
    "\n",
    "- To submit your notebook, save it and then click on the blue submit button at the beginning of the page.\n",
    "\n",
    "Let's get started!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cfe582ed",
   "metadata": {
    "deletable": false,
    "editable": false,
    "id": "dn-6c02VmqiN",
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import pickle\n",
    "import numpy as np\n",
    "import tensorflow as tf\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2eb02f7e",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "import unittests"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "71e64ae9",
   "metadata": {
    "id": "bLTQd84RUs1j"
   },
   "source": [
    "<a name=\"2\"></a>\n",
    "## Dataset exploration\n",
    "\n",
    "Let's explore the dataset in the folder `./PetImages`. There is a subdirectory for each class, so one for dogs and one for cats. The complete tree looks like this:\n",
    "\n",
    "```\n",
    ".\n",
    "└── PetImages/\n",
    "    ├── Cat/\n",
    "    │   ├── cat_1.jpg\n",
    "    │   ├── cat_2.jpg\n",
    "    │   └── ...\n",
    "    └── Dog/\n",
    "        ├── dog_1.jpg\n",
    "        ├── dog_2.jpg\n",
    "        └── ...\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a826680",
   "metadata": {
    "deletable": false,
    "editable": false,
    "id": "DM851ZmN28J3",
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# Directory that holds the data\n",
    "DATA_DIR = './PetImages'\n",
    "\n",
    "# Subdirectories for each class\n",
    "data_dir_dogs = os.path.join(DATA_DIR, 'Dog')\n",
    "data_dir_cats = os.path.join(DATA_DIR, 'Cat')\n",
    "\n",
    "# os.listdir returns a list containing all files under the given dir\n",
    "print(f\"There are {len(os.listdir(data_dir_dogs))} images of dogs.\")\n",
    "print(f\"There are {len(os.listdir(data_dir_cats))} images of cats.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "60586e8e",
   "metadata": {},
   "source": [
    "It is a good start to know how many images there are of each class but let's actually plot some of them to get a better sense of the kinds of images in the dataset:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "09b1d554",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Get the filenames for cats and dogs images\n",
    "cats_filenames = [os.path.join(data_dir_cats, filename) for filename in os.listdir(data_dir_cats)]\n",
    "dogs_filenames = [os.path.join(data_dir_dogs, filename) for filename in os.listdir(data_dir_dogs)]\n",
    "\n",
    "fig, axes = plt.subplots(2, 4, figsize=(14, 7))\n",
    "fig.suptitle('Cat and Dog Images', fontsize=16)\n",
    "\n",
    "# Plot the first 4 images of each class\n",
    "for i, cat_image in enumerate(cats_filenames[:4]):\n",
    "    img = tf.keras.utils.load_img(cat_image)\n",
    "    axes[0, i].imshow(img)\n",
    "    axes[0, i].set_title(f'Example Cat {i}')\n",
    "\n",
    "for i, dog_image in enumerate(dogs_filenames[:4]):\n",
    "    img = tf.keras.utils.load_img(dog_image)\n",
    "    axes[1, i].imshow(img)\n",
    "    axes[1, i].set_title(f'Example Dog {i}')\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b48427be",
   "metadata": {},
   "source": [
    "These sure are cute! **Notice that these images come in all kinds of resolutions!**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "046d48c8",
   "metadata": {},
   "source": [
    "<a name=\"ex01\"></a>\n",
    "## Exercise 1: train_val_datasets\n",
    "\n",
    "Now that you are familiar with the raw data it is time for you to create the datasets that will yield batches for both for training and validation. For this, complete the `train_val_datasets` function below.\n",
    "\n",
    "\n",
    "**Hints:**:\n",
    "\n",
    "- It is recommended to read the documentation for [`tf.keras.utils.image_dataset_from_directory`](https://www.tensorflow.org/api_docs/python/tf/keras/utils/image_dataset_from_directory) since this function provides a lot of functionalities.\n",
    "\n",
    "- You should already know the directory in which the images are stored. You can either use the global variable defined earlier or hardcode the path.\n",
    "\n",
    "- Remember that the images in this dataset come in a variety of resolutions. This can be standardized  by defining the `image_size` parameter which will be used to convert each image to this target resolution. **For this exercise, use a `image_size` of (150, 150)**. \n",
    "\n",
    "- It is recommended to use a `batch_size`  of 128 as it yields a good trade-off between training times and memory usage.\n",
    "- You should select an appropriate value for the `label_mode` parameter given that you are dealing with binary classification.\n",
    "\n",
    "\n",
    "\n",
    "- Since the images haven't been splitted into training and validation sets you should use the `validation_split` parameter for this purpose. **You must use 10% of the images as validation dataset**.\n",
    "- When using the argument `validation_split`, there are two other arguments that **must be passed** otherwise an exception will be thrown. They are:\n",
    "- - `seed`, a random seed to avoid overlapping files in training and validation (any positive integer is fine)\n",
    "- - `subset`, which subset should be returned (training, validation or both). You must use the correct value given the context.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "de9d37da",
   "metadata": {
    "deletable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# GRADED FUNCTION: train_val_datasets\n",
    "\n",
    "def train_val_datasets():\n",
    "    \"\"\"Creates datasets for training and validation.\n",
    "\n",
    "    Returns:\n",
    "        (tf.data.Dataset, tf.data.Dataset): Training and validation datasets.\n",
    "    \"\"\"\n",
    "\n",
    "    ### START CODE HERE ###\n",
    "\n",
    "    training_dataset, validation_dataset = tf.keras.utils.image_dataset_from_directory( \n",
    "        directory=None,\n",
    "        image_size=None,\n",
    "        batch_size=None,\n",
    "        label_mode=None,\n",
    "        validation_split=None,\n",
    "        subset=None,\n",
    "        seed=42 \n",
    "    )\n",
    "\n",
    "    ### END CODE HERE ###\n",
    "\n",
    "    return training_dataset, validation_dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "30126cd1",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Create the datasets\n",
    "training_dataset, validation_dataset = train_val_datasets()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8369efc4",
   "metadata": {},
   "source": [
    "**Expected Output:**\n",
    "\n",
    "```\n",
    "Found 22434 files belonging to 2 classes.\n",
    "Using 20191 files for training.\n",
    "Using 2243 files for validation.\n",
    "```\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "161903ee",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Test your code!\n",
    "unittests.test_train_val_datasets(train_val_datasets)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d2ac76c",
   "metadata": {},
   "source": [
    "<a name=\"3.2\"></a>\n",
    "Now you have two datasets, one for training and another one for validation. In the ungraded labs you applied some extra transformations to the datasets by using methods of `tf.data.Dataset` such as [prefetch](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#prefetch) or [cache](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#cache). These help training be faster but they come at the expense of more memory usage and due to memory limitations of this environment, you will not use them in this assignment.\n",
    "\n",
    "\n",
    "Before proceeding take some time to inspect a batch of the training set, you can do this using the [`take`](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#take) method from a `tf.data.Dataset` while specifying how many batches you want to get. Using this method will yield a tuple with two elements, the first one being the images in the batch and the second one, their respective labels."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1efbc613",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Get the first batch of images and labels\n",
    "for images, labels in training_dataset.take(1):\n",
    "\texample_batch_images = images\n",
    "\texample_batch_labels = labels\n",
    "\n",
    "print(f\"Maximum pixel value of images: {np.max(example_batch_images)}\\n\")\n",
    "print(f\"Shape of batch of images: {example_batch_images.shape}\")\n",
    "print(f\"Shape of batch of labels: {example_batch_labels.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac6601d8",
   "metadata": {},
   "source": [
    "Notice that the pixel values of the images have not yet been normalized at this point so **you must add a Rescaling layer in your model, to apply a factor of 1./255 to these values.**"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f82db0f7",
   "metadata": {
    "id": "TI3oEmyQCZoO"
   },
   "source": [
    "<a name=\"ex02\"></a>\n",
    "## Exercise 2: create_model\n",
    "\n",
    "With the data ready, your next task is to define the architecture of the model that will be trained. Complete the `create_model` function below.\n",
    "\n",
    "Aside from defining the architecture of the model, you should also compile it so make sure to use a `loss` function that is compatible with the `label_mode` you defined in the previous exercise, which should also be compatible with the last layer of your network. You can tell if they aren't compatible if you get an error during training.\n",
    "\n",
    "**Hints:** \n",
    "\n",
    "- **You should use at least 3 convolution layers to achieve the desired performance. This is a necessary condition for this function to pass the grading.**\n",
    "\n",
    "- **The FIRST layer must be a [tf.keras.layers.Input](https://www.tensorflow.org/api_docs/python/tf/keras/Input) with the appropriate shape argument.**\n",
    "\n",
    "- **The SECOND layer of your model MUST be the rescaling layer, i.e, [`tf.keras.layers.Rescaling`](https://www.tensorflow.org/api_docs/python/tf/keras/layers/Rescaling), with the suitable rescaling parameter, as discussed above.**\n",
    "- **Be mindful about the last layer and its activation function, as it will impact the loss you must use.**\n",
    "\n",
    "\n",
    "Notice that in this assignment you will define the Rescaling Layer within the model rather than as a pre-processing step. This can be done either way but including this layer within the model allows you to preserve this step when saving the model for latter use."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d3504ef1",
   "metadata": {
    "deletable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# GRADED FUNCTION: create_model\n",
    "\n",
    "def create_model():\n",
    "    \"\"\"Creates the untrained model for classifying cats and dogs.\n",
    "\n",
    "    Returns:\n",
    "        tf.keras.Model: The model that will be trained to classify cats and dogs.\n",
    "    \"\"\"\n",
    "\n",
    "    ### START CODE HERE ###\n",
    "\n",
    "    model = tf.keras.models.Sequential([ \n",
    "        None,\n",
    "    ]) \n",
    "\n",
    "  \n",
    "    model.compile( \n",
    "        optimizer=None,\n",
    "        loss=None,\n",
    "        metrics=['accuracy'] \n",
    "\t) \n",
    "    \n",
    "    ### END CODE HERE ###\n",
    "\n",
    "    return model"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bcddbbd0",
   "metadata": {},
   "source": [
    "The next cell allows you to check the number of total and trainable parameters of your model and prompts a warning in case these exceeds those of a reference solution, this serves the following 3 purposes listed in order of priority:\n",
    "- Helps you prevent crashing the kernel during training.\n",
    "\n",
    "- Helps you avoid longer-than-necessary training times.\n",
    "- Provides a reasonable estimate of the size of your model. In general you will usually prefer smaller models given that they accomplish their goal successfully.\n",
    "\n",
    "**Notice that this is just informative** and may be very well below the actual limit for size of the model necessary to crash the kernel. So even if you exceed this reference you are probably fine. However, **if the kernel crashes during training or it is taking a very long time and your model is larger than the reference, come back here and try to get the number of parameters closer to the reference.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "12dd57e5",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Create the compiled but untrained model\n",
    "model = create_model()\n",
    "\n",
    "# Check parameter count against a reference solution\n",
    "unittests.parameter_count(model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "632680cc",
   "metadata": {},
   "source": [
    "Check that the architecture you used is compatible with the dataset (you can ignore the warnings prompted by using the GPU):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "85460f43",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "try:\n",
    "\tmodel.evaluate(example_batch_images, example_batch_labels, verbose=False)\n",
    "except:\n",
    "\tprint(\"Your model is not compatible with the dataset you defined earlier. Check that the loss function, last layer and label_mode are compatible with one another.\")\n",
    "else:\n",
    "\tpredictions = model.predict(example_batch_images, verbose=False)\n",
    "\tprint(f\"predictions have shape: {predictions.shape}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1a8b458f",
   "metadata": {},
   "source": [
    "**Expected Output:**\n",
    "\n",
    "```\n",
    "predictions have shape: (batch_size, n_units)\n",
    "```\n",
    "Where `batch_size` is the one you defined and `n_units` is the number of units of the last layer of your model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9920f0a9",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Test your code!\n",
    "unittests.test_create_model(create_model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19f81f4e",
   "metadata": {},
   "source": [
    "<a name=\"4.2\"></a>\n",
    "\n",
    "<a name=\"ex03\"></a>\n",
    "## Exercise 3: EarlyStoppingCallback\n",
    "\n",
    "With the model's architecture now ready, it is time for you to define a callback to stop training if the following conditions are satisfied:\n",
    "\n",
    "- Training accuracy greater than or equal to 95%\n",
    "\n",
    "\n",
    "- Validation accuracy greater than or equal to 80%\n",
    "\n",
    "For this, complete the `EarlyStoppingCallback` class below. Remember from course 1 that you must create a class that inherits from `tf.keras.callbacks.Callback` and you must add a method called `on_epoch_end` with the correct signature to add the stop conditions. \n",
    "\n",
    "**Hints**:\n",
    "\n",
    "- You may look at the documentation for [`tf.keras.callbacks.Callback`](https://www.tensorflow.org/api_docs/python/tf/keras/callbacks/Callback)\n",
    "\n",
    "\n",
    "- Whenever you compile a model with a metric and provide validation data when training that model, TensorFlow will automatically create another metric and append `val_` to its name. Since your model was compiled with the `accuracy` metric and you are using a validation dataset, you will have access to a metric called `val_accuracy`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "95869d7f",
   "metadata": {
    "deletable": false,
    "tags": [
     "graded"
    ]
   },
   "outputs": [],
   "source": [
    "# GRADED CLASS: EarlyStoppingCallback\n",
    "\n",
    "### START CODE HERE ###\n",
    "\n",
    "# Remember to inherit from the correct class\n",
    "class EarlyStoppingCallback():\n",
    "\n",
    "    # Define the correct function signature for on_epoch_end method\n",
    "    def None():\n",
    "        # Check if the accuracy is greater or equal to 0.95 and validation accuracy is greater or equal to 0.8\n",
    "        if None >= None and None >= None:\n",
    "            None = None\n",
    "\n",
    "            print(\"\\nReached 95% train accuracy and 80% validation accuracy, so cancelling training!\")\n",
    "\n",
    "### END CODE HERE ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6c13e6b0",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Test your code!\n",
    "unittests.test_EarlyStoppingCallback(EarlyStoppingCallback)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "81504310",
   "metadata": {
    "id": "SMFNJZmTCZv6"
   },
   "source": [
    "<a name=\"5\"></a>\n",
    "\n",
    "Now it is time to train your model! The callback should fire before reaching 15 epochs, if this is not the case, try a different architecture for your model!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "12a9f0d5",
   "metadata": {
    "deletable": false,
    "editable": false,
    "id": "5qE1G6JB4fMn",
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Train the model and save the training history (this may take some time)\n",
    "history = model.fit(\n",
    "\ttraining_dataset,\n",
    "\tepochs=15,\n",
    "\tvalidation_data=validation_dataset,\n",
    "\tcallbacks = [EarlyStoppingCallback()]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4078aa3e",
   "metadata": {
    "id": "VGsaDMc-GMd4"
   },
   "source": [
    "<a name=\"5.1\"></a>\n",
    "Once training has finished, you can run the following cell to check the training and validation accuracy achieved at the end of each epoch."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5965a924",
   "metadata": {
    "deletable": false,
    "editable": false,
    "id": "MWZrJN4-65RC",
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Get training and validation accuracies\n",
    "acc = history.history['accuracy']\n",
    "val_acc = history.history['val_accuracy']\n",
    "loss = history.history['loss']\n",
    "val_loss = history.history['val_loss']\n",
    "\n",
    "# Get number of epochs\n",
    "epochs = range(len(acc))\n",
    "\n",
    "fig, ax = plt.subplots(1, 2, figsize=(10, 5))\n",
    "fig.suptitle('Training and validation accuracy')\n",
    "\n",
    "for i, (data, label) in enumerate(zip([(acc, val_acc), (loss, val_loss)], [\"Accuracy\", \"Loss\"])):\n",
    "    ax[i].plot(epochs, data[0], 'r', label=\"Training \" + label)\n",
    "    ax[i].plot(epochs, data[1], 'b', label=\"Validation \" + label)\n",
    "    ax[i].legend()\n",
    "    ax[i].set_xlabel('epochs')\n",
    "\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d7149023",
   "metadata": {
    "id": "NYIaqsN2pav6"
   },
   "source": [
    "You will probably encounter that the model is overfitting, which means that it is doing a great job at classifying the images in the training set but struggles with new data. This is perfectly fine and you will learn how to mitigate this issue in the upcoming week.\n",
    "\n",
    "**To pass this assignment your model should have achieved a training accuracy of 95% and a validation accuracy of 80%**. If your model didn't achieve these thresholds, try training again with a different model architecture and remember to use at least 3 convolutional layers.\n",
    "\n",
    "<a name=\"6\"></a>\n",
    "\n",
    "**Before submitting your assignment, please run the following code to save your training history as it will be used in grading.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "56b61aa8",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "with open('history.pkl', 'wb') as f:\n",
    "    pickle.dump(history.history, f)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aa28a2d1",
   "metadata": {
    "id": "joAaZSWWpbOI"
   },
   "source": [
    "**Congratulations on finishing this week's assignment!**\n",
    "\n",
    "You have successfully implemented a convolutional neural network that classifies images of cats and dogs, along with the helper functions needed to pre-process the images!\n",
    "\n",
    "**Keep it up!**"
   ]
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "grader_version": "1",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
